<?php

Raise::load('core.RaiseHelper');
Raise::load('core.RaiseHttpHeaderCollection');
Raise::load('core.RaiseApplication');
Raise::load('core.RaiseTypedCollection');
Raise::load('core.RaiseText');

/**
 * RaiseHttpResponse class
 * defines a HTTP Response
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core.HTTP
 * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html
 * @since 1.2
 */
class RaiseHttpResponse extends RaiseHelper {

    const HTTP_1_1 = 'HTTP/1.1';
    const HTTP_1_0 = 'HTTP/1.0';

    const HTTP_CODE_100 = '100 Continue';
    const HTTP_CODE_101 = '101 Switching Protocols';
    const HTTP_CODE_102 = '102 Processing';

    const HTTP_CODE_200 = '200 OK';
    const HTTP_CODE_201 = '201 Created';
    const HTTP_CODE_202 = '202 Accepted';
    const HTTP_CODE_203 = '203 Non-Authoritative Information';
    const HTTP_CODE_204 = '204 No Content';
    const HTTP_CODE_205 = '205 Reset Content';
    const HTTP_CODE_206 = '206 Partial Content';
    const HTTP_CODE_207 = '207 Multi-Status';

    const HTTP_CODE_300 = '300 Multiple Choices';
    const HTTP_CODE_301 = '301 Moved Permanently';
    const HTTP_CODE_302 = '302 Found';
    const HTTP_CODE_303 = '303 See Other';
    const HTTP_CODE_304 = '304 Not Modified';
    const HTTP_CODE_305 = '305 Use Proxy';
    const HTTP_CODE_307 = '307 Temporary Redirect';

    const HTTP_CODE_400 = '400 Bad Request';
    const HTTP_CODE_401 = '401 Unauthorized';
    const HTTP_CODE_402 = '402 Payment Required';
    const HTTP_CODE_403 = '403 Forbidden';
    const HTTP_CODE_404 = '404 Not Found';
    const HTTP_CODE_405 = '405 Method Not Allowed';
    const HTTP_CODE_406 = '406 Not Acceptable';
    const HTTP_CODE_407 = '407 Proxy Authentication Required';
    const HTTP_CODE_408 = '408 Request Time-out';
    const HTTP_CODE_409 = '409 Conflict';
    const HTTP_CODE_410 = '410 Gone';
    const HTTP_CODE_411 = '411 Length Required';
    const HTTP_CODE_412 = '412 Precondition Failed';
    const HTTP_CODE_413 = '413 Request Entity Too Large';
    const HTTP_CODE_414 = '414 Request-URI Too Large';
    const HTTP_CODE_415 = '415 Unsupported Media Type';
    const HTTP_CODE_416 = '416 Requested Range Not Satisfiable';
    const HTTP_CODE_417 = '417 Expectation Failed';
    const HTTP_CODE_418 = '418 I\'m a teapot'; // april fools :D
    const HTTP_CODE_422 = '422 Unprocessable Entity';
    const HTTP_CODE_423 = '423 Locked';
    const HTTP_CODE_424 = '424 Failed Dependency';
    const HTTP_CODE_425 = '425 Unordered Collection';
    const HTTP_CODE_426 = '426 Upgrade Required';
    const HTTP_CODE_444 = '444 No Response';
    const HTTP_CODE_449 = '449 Retry With';
    const HTTP_CODE_450 = '423 Blocked By Windows Parental Controls';
    const HTTP_CODE_499 = '499 Client Closed Request';

    const HTTP_CODE_500 = '500 Internal Server Error';
    const HTTP_CODE_501 = '501 Not Implemented';
    const HTTP_CODE_502 = '502 Bad Gateway';
    const HTTP_CODE_503 = '503 Service Unavailable';
    const HTTP_CODE_504 = '504 Gateway Time-out';
    const HTTP_CODE_505 = '505 HTTP Version Not Supported';
    const HTTP_CODE_506 = '506 Variant Also Negotiates';
    const HTTP_CODE_507 = '507 Insufficient Storage';
    const HTTP_CODE_509 = '509 Bandwidth Limit Exceeded';
    const HTTP_CODE_510 = '510 Not Extended';

    /**
     * The HTTP Version of the Status-Line in the HTTP response
     * @var string
     */
    private $version = RaiseHttpResponse::HTTP_1_1;

    /**
     * The HTTP Status-Code of the Status-Line in the HTTP response
     * @var string
     */
    private $code = RaiseHttpResponse::HTTP_CODE_200;

    /**
     * Body of the HTTP Response
     * @var string
     */
    private $body = '';

    /**
     * An array of the HTTP headers in the HTTP Response
     * @var RaiseHttpHeaderCollection
     */
    private $headers;

    /**
     * Flags whether the headers has been sent out or not.
     * @var boolean
     */
    protected $headersFlushed = false;

    /**
     * The RaiseApplication that is interacting with this response
     * @var RaiseApplication
     */
    private $application;

    /**
     * Cookies pertaining to this request
     * @var RaiseTypedCollection
     */
    private $cookies;

    /**
     * Create a new RaiseHttpResponse
     * @param RaiseApplication $app The application that is holding this HTTP response
     * @param string $s (optional) The HTTP response string to parse from.
     */
    public function __construct($app, $s = null){
        $this->headers = new RaiseHttpHeaderCollection();
        if($s){
            $this->parse($s);
        }
        $this->application($app);
        $this->cookies = new RaiseTypedCollection('RaiseCookie');
    }

    /**
     * Parse the HTTP response in the form of a string into this object
     * @param string $s
     */
    private function parse($s){
        $s = RaiseText::neutralizeNewlines($s);
        $lines = explode(RaiseText::NEWLINE_NIX, $s);
        if(count($lines) > 1){
            $statusLine = $lines[0];
            $sp = strpos($statusLine, ' ');
            if($sp === false){
                // error
            }else{
                $this->version(trim(substr($statusLine, 0, $sp)));
                $this->code(trim(substr($statusLine, $sp + 1)));
            }
            $last = count($lines)-1;
            $this->body($lines[$last]);
            unset($lines[0]);
            unset($lines[$last]);
            unset($last);

            foreach($lines as $l){
                $c = strpos($l, ':');
                if($c){
                    $key = trim(substr($l, 0, $c));
                    $value = trim(substr($l, $c + 1));
                    $this->headers()->add($key, $value, false);
                }
            }
        }
    }

    /**
     * Get or set the status code of the HTTP response
     * @param string $v (optional) If set, the new value will be set.
     * @return string
     */
    public function code($c = null){
        if(func_num_args() == 1){
            $this->code = $c;
        }
        return $this->code;
    }

    /**
     * Get or set the version of the HTTP response
     * @param string $v (optional) If set, the new value will be set.
     * @return string
     */
    public function version($v = null){
        if(func_num_args() == 1){
            $this->version = $v;
        }
        return $this->version;
    }

    /**
     * Get or set the body of the HTTP response
     * @param string $b (optional) If set, the new value will be set.
     * @return string
     */
    public function body($b = null){
        if(func_num_args() == 1){
            $this->body = $b;
        }
        return $this->body;
    }

    /**
     * Get or set the RaiseApplication that is interacting with this response
     * @param RaiseApplication $app (optional) If set, the new value will be set.
     * @return RaiseApplication
     */
    public function application($app = null){
        if(func_num_args() == 1){
            if(!($app instanceof RaiseApplication)){
                throw new RaiseInvalidArgumentException('RaiseHttpResponse::application() expecting $app to be an instance of RaiseApplication, ' . RaiseVariable::typeInfo($app) . ' given instead.');
            }
            $this->application = $app;
        }
        return $this->application;
    }


    /**
     * Get a particular Cookie value using its corresponding key or the entire collection of Cookies that will be sent to the browser in the HTTP response
     * @return RaiseTypedCollection
     */
    public function cookies(){
        return $this->cookies;
    }

    /**
     * Get or set the RaiseHttpHeaderCollection that contains all the headers of the HTTP response
     * @param RaiseHttpHeaderCollection $h (optional) If set, the new value will be set.
     * @return RaiseHttpHeaderCollection
     */
    public function headers($h = null){
        if(func_num_args() == 1){
            if(!($h instanceof RaiseHttpHeaderCollection)){
                throw new RaiseInvalidArgumentException('Invalid parameter passed when setting headers property. Expecting RaiseHttpHeaderCollection.');
            }
            $this->headers = $h;
        }
        return $this->headers;
    }

    /**
     * Do a redirect
     * @param string $url A URL to redirect to
     */
    public function redirect($url) {
        $this->application->plugin()->callHook('raise.http.response.redirecting', $this);
        if(substr($url, 0, 1) == '/'){
            $url = $this->application->settings()->get('phpRaise', 'baseUrl') . substr($url, 1);
        }
        $this->headers->add('Location', $url, true);
        $this->flushHeaders();
        $this->application->plugin()->callHook('raise.http.response.redirected', $this);
        exit;
    }

    /**
     * Flushes all headers and write all buffer to browser
     */
    public function output() {
        $this->flushHeaders();
        $this->application->plugin()->callHook('raise.http.response.output.flushing', $this);
        echo $this->body();
        $this->application->plugin()->callHook('raise.http.response.output.flushed', $this);
    }

    /**
     * Flushes all headers to the browser
     * This function can only be called once.
     */
    public function flushHeaders(){
        if(!$this->headersFlushed){
            $this->application->plugin()->callHook('raise.http.response.headers.flushing', $this);
            foreach ($this->headers as $k => $h) {
                if (is_array($h) && is_string($k)) {
                    foreach ($h as $d) {
                        header($k . ': ' . $d);
                    }
                }elseif(is_string($k)){
                    header($k . ': ' . $h);
                }else{
                    header($h);
                }
            }
            $this->application->plugin()->callHook('raise.http.response.headers.flushed', $this);
            foreach($this->cookies as $cookie){
                $cookie->set();
            }
            $this->headersFlushed = true;
        }
    }

    public function __toString(){
        $buffer = '';
        $buffer = $this->version() .' '.$this->code() . "\n";
        foreach ($this->headers() as $k => $h) {
            if (is_array($h)) {
                foreach ($h as $d) {
                    $buffer .= $k . ': ' . $d . "\n";
                }
            } else {
                    $buffer .= $k . ': ' . $h . "\n";
            }
        }
        $buffer .= "\n" . $this->body;
        return $buffer;
    }

}